package library;

import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Hashtable;
import java.util.LinkedList;

import udp.UdpClient;
import udp.UdpServer;
import udp.message.CreateAccountMessage;
import udp.message.GetMissingRequestMessage;
import udp.message.GetNonReturnersMessage;
import udp.message.LibraryMessage;
import udp.message.ReserveBookInterMessage;
import udp.message.ReserveBookMessage;
import udp.message.SetDurationMessage;
import udp.message.UdpMessage;
import util.ConfigurationHelper;
import dom.EducationalSystemEnum;

/**
 * The library server
 * @author Alex Perkins
 *
 */
public class LibraryServer extends UdpServer {
	
	private Library library;
	private LibraryServerService libraryService;
	private String serverId;
	private String replicaId;
	private static int sequenceNumber = 0;
	private boolean debugMode = false;
	private boolean pendingFail = false;
	private Hashtable<Integer,UdpMessage> commandHistory;
	private LinkedList<UdpMessage> commandQueue;

	/**
	 * Anonymous library server
	 */
	public LibraryServer() {
		super("defaultServerId",-1);
		this.debugMode = false;
	}

	/**
	 * Library server with identifier
	 * @param id of the server
	 */
	public LibraryServer(EducationalSystemEnum edu, String replicaId) {
		super(edu.toString(), LibraryProperties.getPortById(edu));
		
		try{
			// instantiate command queue
			commandQueue = new LinkedList<UdpMessage>();
			// Instantiate command history hastable
			commandHistory = new Hashtable<Integer,UdpMessage>();
			// debug mode
			debugMode = Boolean.parseBoolean(ConfigurationHelper.getProperty("debug"));
			// set server id
			serverId = edu.toString();
			// set which replica this library server is part of
			this.replicaId = replicaId;
			// create new library based on the edu
			library = new Library(edu,this.replicaId);
			// create library service
			libraryService = new LibraryServerService(library);
			// set which replica this library server is part of
			// start the UDP server
			startUDPServer();
			//createServerInstance(edu);
		}
		catch(Exception ex) {
			ex.printStackTrace();
			print("Error starting UDP server");
		}
	}
	
	/**
	 * Creates the server instance
	 * @param edu
	 */
	private void createServerInstance(EducationalSystemEnum edu, String replicaId){
		
		try{
			// create new library based on the edu
			library = new Library(edu,replicaId);
			// create library service
			libraryService = new LibraryServerService(library);
		}
		catch(Exception e){
			print("problem creating library instance");
		}
	}

	/**
	 * Handles sending heartbeats to the other servers
	 */
	
	@Override
	public void receiveMessage(UdpMessage message) {
		print("LibraryServer receiveMessage");

		if(message != null) {
			
			if(message instanceof LibraryMessage){
				
				LibraryMessage command = (LibraryMessage) message;
				
				/////////////////////// HACK
				boolean doCheckSequence = true;
				if(command instanceof GetNonReturnersMessage){
					GetNonReturnersMessage gnrm = (GetNonReturnersMessage) command;
					if(!gnrm.isFirst()){
						doCheckSequence = false;
					}
				}
				/////////////////////// HACK
				
				if(doCheckSequence){
					checksequence(command);
				}
				else{
					handleLibraryCommand(command);
				}

				//commandQueue.add(action);
			} 
			else{
				String msg = "unknown UdpMessage instance [";
				if(message.getClass()!=null && message.getClass().getName() != null){
					msg += message.getClass().getName();
				}
				msg += "]";
				print(msg);
			}
		}
		else{
			print("received a LibraryMessage that is null");
		}
		
	}
	
	

	/**
	 * Process the command
	 * @param message
	 */
	public void handleLibraryCommand(LibraryMessage message){
		
		debug("received command: " + message.toString());
		
		if(message instanceof CreateAccountMessage){
			CreateAccountMessage createAccountMessage = (CreateAccountMessage) message;
			processCreateAccountMessage(createAccountMessage);
		}
		else if(message instanceof GetNonReturnersMessage){
			GetNonReturnersMessage getNonReturnersMessage = (GetNonReturnersMessage) message;
			processGetNonReturnersMessage(getNonReturnersMessage);
		}
		else if(message instanceof ReserveBookInterMessage){
			ReserveBookInterMessage reserveBookInterMessage = (ReserveBookInterMessage) message;
			processReserveBookInterMessage(reserveBookInterMessage);
		}
		else if(message instanceof ReserveBookMessage){
			ReserveBookMessage reserveBookMessage = (ReserveBookMessage) message;
			processReserveBookMessage(reserveBookMessage);
		}
		else if(message instanceof SetDurationMessage){
			SetDurationMessage setDurationMessage = (SetDurationMessage) message;
			processSetDurationMessage(setDurationMessage);
		}
		else if (message instanceof GetMissingRequestMessage){
			GetMissingRequestMessage getmissingsequence = (GetMissingRequestMessage) message;
			processReturnMissingSeqeunce(getmissingsequence);
		}

		else{
			print("Unrecognized command: " + message.toString());
		}
	}
	
	

	/**
	 * Handles the actions required to be taken when a create account message is received
	 * @param createAccountMessage
	 */
	private void processCreateAccountMessage(CreateAccountMessage createAccountMessage){
		
		String result = libraryService.createAccount(createAccountMessage.getFirstName(),
				createAccountMessage.getLastName(),
				createAccountMessage.getEmailAddress(), 
				createAccountMessage.getPhoneNumber(), 
				createAccountMessage.getUsername(), 
				createAccountMessage.getPassword(),
				createAccountMessage.getEdu());
		
		if (pendingFail) {
			result = "System failure";

			pendingFail = false;
		}
		print("SENDING RESULT:  [ " +result+" ]");
		createAccountMessage.setResult(result);
		// send the reply

		createAccountMessage.setReplicaId(this.replicaId);
		createAccountMessage.setDestHost(ConfigurationHelper.getProperty("frontend.host"));
		//send to sequencer port
		createAccountMessage.setDestPort(ConfigurationHelper.getIntProperty("frontend.port"));
		send(createAccountMessage);
	}
	
	/**
	 * Handles the actions required to be taken when a get missing message request is received
	 * @param getMissingMessageRequest
	 */

	
	/**
	 * Handles the actions required to be taken when a get non returners message is received
	 * @param getNonReturnersMessage
	 */
	private void processGetNonReturnersMessage(GetNonReturnersMessage getNonReturnersMessage){

		boolean isMessageFromFrontEnd = getNonReturnersMessage.isFirst();
		
		if(isMessageFromFrontEnd){
			
			print("THE MESSAGE IS FROM THE FRONT END");
			
			String result = libraryService.getNonReturners(getNonReturnersMessage.getUsername(),
					getNonReturnersMessage.getPassword(),
					getNonReturnersMessage.getEi(),
					getNonReturnersMessage.getNumDays(),
					false);
			
			StringBuilder sb = new StringBuilder();
			
			//getNonReturnersMessage.setResult(result);
			// swap host and port info to send the result back
			//getNonReturnersMessage.setReplicaId(this.replicaId);
			//getNonReturnersMessage.setDestHost(ConfigurationHelper.getProperty("frontend.host"));
			//getNonReturnersMessage.setDestPort(ConfigurationHelper.getIntProperty("frontend.port"));
			// send the reply
			
			getNonReturnersMessage.setResult(result);
			// send the reply

			getNonReturnersMessage.setReplicaId(this.replicaId);
			getNonReturnersMessage.setDestHost(ConfigurationHelper.getProperty("frontend.host"));
			//send to sequencer port
			getNonReturnersMessage.setDestPort(ConfigurationHelper.getIntProperty("frontend.port"));
			send(getNonReturnersMessage);
		}
		
		else{
			
			print("THE MESSAGE IS NOT FROM THE FRONT END");
			
			String result = libraryService.getNonReturners(getNonReturnersMessage.getUsername(),
					getNonReturnersMessage.getPassword(),
					getNonReturnersMessage.getEi(),
					getNonReturnersMessage.getNumDays(),
					true);
			
			getNonReturnersMessage.setResult(result);
			getNonReturnersMessage.setDestPort(getNonReturnersMessage.getSrcPort());
			getNonReturnersMessage.setDestHost(getNonReturnersMessage.getSrcHost());
			getNonReturnersMessage.setDestLibrary(getNonReturnersMessage.getSrcLibrary());
			
			print("going to send to [" + getNonReturnersMessage.getDestHost() + ":" + getNonReturnersMessage.getSocketPort() + "]");
			
			sendReply(getNonReturnersMessage);
		}
		
		/*
		 * StringBuilder sb = new StringBuilder();	
		sb.append("#############################################\n");
		sb.append("## THE LIST OF NON-RETURNERS:\n");
		sb.append("#############################################\n");
		sb.append(nonReturnersToReturn);
		sb.append("#############################################\n");
		debug("----END OF GET NON RETURNERS ACTION ----");
		 */
	}
	
	/**
	 * Handles the actions required to be taken when a reserver inter-library message is received
	 * @param reserveBookInterMessage
	 */
	private void processReserveBookInterMessage(ReserveBookInterMessage reserveBookInterMessage){
		
		String result = libraryService.reserveInterLibrary(reserveBookInterMessage.getUsername(),
				reserveBookInterMessage.getPassword(), 
				reserveBookInterMessage.getBookName(),
				reserveBookInterMessage.getAuthorName());
		
		reserveBookInterMessage.setResult(result);
		// swap host and port info to send the reply
		reserveBookInterMessage.setReplicaId(this.replicaId);
		reserveBookInterMessage.setDestHost(ConfigurationHelper.getProperty("frontend.host"));
		reserveBookInterMessage.setDestPort(ConfigurationHelper.getIntProperty("frontend.port"));
		send(reserveBookInterMessage);
	}
	
	/**
	 * Handles the actions required to be taken when a reserve book message is received
	 * @param reserveBookMessage
	 */
	private void processReserveBookMessage(ReserveBookMessage reserveBookMessage){
		
		String result = libraryService.reserveBook(reserveBookMessage.getUsername(),
				reserveBookMessage.getPassword(), 
				reserveBookMessage.getBookName(),
				reserveBookMessage.getAuthorName());
		
		reserveBookMessage.setResult(result);
		// swap host and port info in order to send a reply
		reserveBookMessage.setReplicaId(this.replicaId);
		reserveBookMessage.setDestHost(ConfigurationHelper.getProperty("frontend.host"));
		reserveBookMessage.setDestPort(ConfigurationHelper.getIntProperty("frontend.port"));
		send(reserveBookMessage);
	}
	
	/**
	 * Handles the actions required to be taken when a set duration message is received
	 * @param setDurationMessage
	 */
	private void processSetDurationMessage(SetDurationMessage setDurationMessage){
		
		//TODO: add a return result DONE
		String result =libraryService.setDuration(setDurationMessage.getUsername(),
				setDurationMessage.getBookName(),
				setDurationMessage.getNumDays());
		
		setDurationMessage.setResult(result);
		setDurationMessage.setReplicaId(this.replicaId);
		setDurationMessage.setDestHost(ConfigurationHelper.getProperty("frontend.host"));
		setDurationMessage.setDestPort(ConfigurationHelper.getIntProperty("frontend.port"));
		send(setDurationMessage);
	}
	

	
	/**
	 * Validates a message
	 * @param message
	 * @return
	 */
	private boolean validateLibraryMessage(LibraryMessage message){
		
		// make sure that the src and dest libraries exist
		EducationalSystemEnum edu;
		try{
			edu = EducationalSystemEnum.valueOf(message.getSrcLibrary());
		}
		catch(Exception e){
			print("source library '" + message.getSrcLibrary() + "' does not exists");
			return false;
		}
		
		try{
			edu = EducationalSystemEnum.valueOf(message.getDestLibrary());
		}
		catch(Exception e){
			print("destination library '" + message.getSrcLibrary() + "' does not exists");
			return false;
		}
		
		return true;
		
	}
	
	private void printToServerLog(String msg){
		System.out.println("[" + library.getEducationalInstitutionString() + "] " + msg);
	}
	
	/**
	 * Takes care of sending a message using the udp client
	 * @param msg
	 */
	private void send(UdpMessage msg){
		try{
			// set the src information before sending
			msg.setSrcHost(LibraryProperties.getHostnameById(serverId));
			msg.setSrcPort(LibraryProperties.getPortById(serverId));
			// create the udp client
			UdpClient udpClient = new UdpClient(msg.getDestHost(),msg.getDestPort());
			// send the message
			udpClient.send(msg);
		}
		catch(UnknownHostException e){
			print("uknown host:" + e.getMessage());
		}
		catch(SocketException e){
			print("couldn't connect to socket:" + e.getMessage());
		}
	}
	
	
	private void sendReply(UdpMessage msg){
		try{
			// set the src information before sending
			msg.setSrcHost(LibraryProperties.getHostnameById(serverId));
			msg.setSrcPort(LibraryProperties.getPortById(serverId));
			// create the udp client
			UdpClient udpClient = new UdpClient(msg.getDestHost(),msg.getSocketPort());
			// send the message
			udpClient.send(msg);
		}
		catch(UnknownHostException e){
			print("uknown host:" + e.getMessage());
		}
		catch(SocketException e){
			print("couldn't connect to socket:" + e.getMessage());
		}
	}
	
	/**
	 * Sends a UDP message to the server and port number
	 * @param serverId
	 * @param port
	 * @param msg
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private UdpMessage sendAndReceive(String serverId, int port, UdpMessage request) throws IOException, ClassNotFoundException {
		UdpClient udpClient = new UdpClient(ConfigurationHelper.getProperty("host.name",serverId),port);
		UdpMessage response = udpClient.sendAndReceive(request);
		return response;
	}
	
	/**
	 * Sends a UDP message to the other library servers
	 * @param message
	 */
	public void multicast(UdpMessage message){
		
		for(EducationalSystemEnum key : LibraryProperties.getUDPPorts().keySet()){
			if(!key.toString().equals(library.getEducationalInstitutionString())){
				try{
					message.setDestHost(key.toString());
					String host = LibraryProperties.getUDPHosts().get(key);
					int port = LibraryProperties.getUDPPorts().get(key);
					debug("using [" + host + ":"+port + "]");
					UdpClient client = new UdpClient(host,port);
					client.send(message);
				}
				catch(Exception ex){
					print("Couldn't send message: " + ex.getMessage());
				}
			}
		}
		
	}
	
	/**
	 * Actions to take before stopping the server
	 */
	public void stopServer(){
		stopUDP();
	}
	
	/**
	 * Increments the expected command sequence number
	 */
	private synchronized void incrementSequenceNumber(){
		sequenceNumber += 1;
	}
	
	private boolean isNextSequenceNumber(double sequenceNumber){
		double tmpSeq = (this.sequenceNumber+1);
		return (tmpSeq == sequenceNumber);
	}
	
	/**
	 * Checks if the server is in debug mode before printing to console
	 * @param msg
	 */
	private void debug(String msg){
		if(debugMode){
			print(msg);
		}
	}
	
	/**
	 * Prints
	 * @param msg
	 */
	private void print(String msg){
		System.out.println("["+id+"] " + msg);
	}
	

	public void nullifyDatabase() {
		library.nullifyDatabase();
	}
	
	private void checksequence(UdpMessage message){

		int messageSeq = message.getSequenceNumber();
		if(isNextSequenceNumber(messageSeq)){
			commandHistory.put(messageSeq,message);
			incrementSequenceNumber();
			doLibraryCommand(message);
		}
		else {
			commandQueue.add(message);
			requestMissingsequence(messageSeq,message.getDestHost());
		}
	}
	
	private void doLibraryCommand(UdpMessage message){
		LibraryMessage command = (LibraryMessage) message;
		handleLibraryCommand(command);
	}
	
	private void requestMissingsequence(int sequenceNumber,String ownHost){
		String[] replicas = {"r1","r2","r3","r4"};
		UdpMessage messagereply = null;
		GetMissingRequestMessage message = new GetMissingRequestMessage(sequenceNumber);
		for(String r : replicas){
			if(!ownHost.equals(ConfigurationHelper.getProperty(r))){
				UdpClient replicaToGetMissingSequence = null;
				try {
					replicaToGetMissingSequence = new UdpClient(ConfigurationHelper.getProperty(r), ConfigurationHelper.getIntProperty(r+ ".replica.port"));
				} catch (SocketException | UnknownHostException e1) {
					e1.printStackTrace();
				}
				try {
					messagereply = replicaToGetMissingSequence.sendAndReceive(message, 1000);
					break;	

				} catch(SocketTimeoutException e) {
					continue;
				}
			}
		}
		processMissingSequence(messagereply);
	
	}
	
	private void processMissingSequence(UdpMessage message){
		checksequence(message);
		while (commandQueue.size()!=0){
			checksequence(commandQueue.getFirst());
		 commandQueue.removeFirst();
		}
	}

	private void processReturnMissingSeqeunce(GetMissingRequestMessage message){
		
		UdpMessage missingMessage =commandHistory.get(message.getSequence());
		//set the source port, dont know if need to chagne source host. will need to debug
		missingMessage.setSrcPort(this.port);
		try {
			UdpClient returnMissing = new UdpClient(message.getSrcHost(), message.getSrcPort());
			returnMissing.send(missingMessage);
		} catch (SocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
	
	public LibraryServerService getLibraryServerService(){
		return libraryService;
	}
}
